 /*******************************************************************************
  * Copyright (c) 2000, 2005 IBM Corporation and others.
  * All rights reserved. This program and the accompanying materials
  * are made available under the terms of the Eclipse Public License v1.0
  * which accompanies this distribution, and is available at
  * http://www.eclipse.org/legal/epl-v10.html
  *
  * Contributors:
  * IBM Corporation - initial API and implementation
  *******************************************************************************/
 package org.eclipse.update.internal.security;

 import java.io.File ;
 import java.io.IOException ;
 import java.io.InputStream ;
 import java.security.KeyStore ;
 import java.security.KeyStoreException ;
 import java.security.NoSuchAlgorithmException ;
 import java.security.cert.Certificate ;
 import java.security.cert.CertificateException ;
 import java.util.ArrayList ;
 import java.util.Enumeration ;
 import java.util.Iterator ;
 import java.util.List ;
 import java.util.jar.JarEntry ;
 import java.util.jar.JarFile ;
 import java.util.zip.ZipException ;

 import org.eclipse.core.runtime.CoreException;
 import org.eclipse.core.runtime.IProgressMonitor;
 import org.eclipse.osgi.util.NLS;
 import org.eclipse.update.core.ContentReference;
 import org.eclipse.update.core.IFeature;
 import org.eclipse.update.core.IVerificationResult;
 import org.eclipse.update.core.IVerifier;
 import org.eclipse.update.core.InstallMonitor;
 import org.eclipse.update.core.JarContentReference;
 import org.eclipse.update.core.Utilities;
 import org.eclipse.update.core.Verifier;
 import org.eclipse.update.internal.core.Messages;
 import org.eclipse.update.internal.core.UpdateCore;
 import org.eclipse.update.internal.core.connection.ConnectionFactory;

 /**
  * The JarVerifier will check the integrity of the JAR.
  * If the Jar is signed and the integrity is validated,
  * it will check if one of the certificate of each file
  * is in one of the keystore.
  *
  */

 public class JarVerifier extends Verifier {

     private static final String MANIFEST = "META-INF"; //$NON-NLS-1$

     private JarVerificationResult result;
     private List /*of CertificatePair*/
     trustedCertificates;
     private boolean acceptUnsignedFiles;
     private List /* of KeyStore */
     listOfKeystores;
     private IProgressMonitor monitor;
     private File jarFile;

     private static byte[] buffer = new byte[8192];
     
     /*
      * Default Constructor
      */
     public JarVerifier() {
         initialize();
     }

     /*
      * Returns the list of the keystores.
      */
     private List getKeyStores() throws CoreException {
         if (listOfKeystores == null) {
             listOfKeystores = new ArrayList (0);
             KeyStores listOfKeystoreHandles = new KeyStores();
             InputStream in = null;
             KeyStore keystore = null;
             KeystoreHandle handle = null;
             while (listOfKeystoreHandles.hasNext()) {
                 try {
                     handle = listOfKeystoreHandles.next();
                     in = ConnectionFactory.get(handle.getLocation()).getInputStream();
                     try {
                         keystore = KeyStore.getInstance(handle.getType());
                         keystore.load(in, null); // no password
 } catch (NoSuchAlgorithmException e) {
                         throw Utilities.newCoreException(NLS.bind(Messages.JarVerifier_UnableToFindEncryption, (new String [] { handle.getLocation().toExternalForm() })), e);
                     } catch (CertificateException e) {
                         throw Utilities.newCoreException(NLS.bind(Messages.JarVerifier_UnableToLoadCertificate, (new String [] { handle.getLocation().toExternalForm() })), e);
                     } catch (KeyStoreException e) {
                         throw Utilities.newCoreException(NLS.bind(Messages.JarVerifier_UnableToFindProviderForKeystore, (new String [] { handle.getType() })), e);
                     } finally {
                         if (in != null) {
                             try {
                                 in.close();
                             } catch (IOException e) {
                             } // nothing
 }
                     } // try loading a keyStore

                     // keystore was loaded
 listOfKeystores.add(keystore);
                 } catch (IOException e) {
                     // nothing... if the keystore doesn't exist, continue
 }

             } // while all key stores

         }

         return listOfKeystores;
     }

     /*
      *
      */
     private void initialize() {
         result = null;
         trustedCertificates = null;
         acceptUnsignedFiles = false;
         listOfKeystores = null;
     }

     /*
      * init
      */
     private void init(IFeature feature, ContentReference contentRef) throws CoreException {
         jarFile = null;
         if (contentRef instanceof JarContentReference) {
             JarContentReference jarReference = (JarContentReference) contentRef;
             try {
                 jarFile = jarReference.asFile();
                 if (UpdateCore.DEBUG && UpdateCore.DEBUG_SHOW_INSTALL)
                     UpdateCore.debug("Attempting to read JAR file:"+jarFile); //$NON-NLS-1$

                 // # of entries
 if (!jarFile.exists()) throw new IOException ();
                 JarFile jar = new JarFile (jarFile);
                 if (jar !=null){
                     try {
                         jar.close();
                     } catch (IOException ex) {
                         // unchecked
 }
                 }
             } catch (ZipException e){
                 throw Utilities.newCoreException(NLS.bind(Messages.JarVerifier_InvalidJar, (new String [] { jarReference.toString() })), e);
             } catch (IOException e) {
                 throw Utilities.newCoreException(NLS.bind(Messages.JarVerifier_UnableToAccessJar, (new String [] { jarReference.toString() })), e);
             }
         }

         result = new JarVerificationResult();
         result.setVerificationCode(IVerificationResult.UNKNOWN_ERROR);
         result.setResultException(null);
         result.setFeature(feature);
         result.setContentReference(contentRef);
     }

     /*
      * Returns true if one of the certificate exists in the keystore
      */
     private boolean existsInKeystore(Certificate cert) throws CoreException {
         try {
             List keyStores = getKeyStores();
             if (!keyStores.isEmpty()) {
                 Iterator listOfKeystores = keyStores.iterator();
                 while (listOfKeystores.hasNext()) {
                     KeyStore keystore = (KeyStore ) listOfKeystores.next();

                     if (keystore.getCertificateAlias(cert) != null) {
                         return true;
                     }
                 }
             }
         } catch (KeyStoreException e) {
             throw Utilities.newCoreException(Messages.JarVerifier_KeyStoreNotLoaded, e);
         }
         return false;
     }

     /*
      *
      */
     private List readJarFile(JarFile jarFile, String identifier)
         throws IOException , InterruptedException {
         List list = new ArrayList ();

         Enumeration entries = jarFile.entries();
         JarEntry currentEntry = null;
         InputStream in = null;
         if (monitor != null)
             monitor.setTaskName(NLS.bind(Messages.JarVerifier_Verify, (new String [] { identifier == null ? jarFile.getName(): identifier })));

         try {
             while (entries.hasMoreElements()) {
                 currentEntry = (JarEntry ) entries.nextElement();
                 list.add(currentEntry);
                 in = jarFile.getInputStream(currentEntry);
                 while ((in.read(buffer, 0, buffer.length)) != -1) {
                     // Security error thrown if tempered
 }
                 if (in!=null)
                     in.close();
             }
         } catch (IOException e) {
             result.setVerificationCode(IVerificationResult.UNKNOWN_ERROR);
             result.setResultException(e);
         } finally {
             try {
                 if (in != null)
                     in.close();
             } catch (IOException e1) {
                 // ignore
 }
         }

         return list;
     }

     /*
      * @param newMonitor org.eclipse.core.runtime.IProgressMonitor
      */
     public void setMonitor(IProgressMonitor newMonitor) {
         monitor = newMonitor;
     }

     /*
      * @see IVerifier#verify(IFeature,ContentReference,boolean, InstallMonitor)
      */
     public IVerificationResult verify(
         IFeature feature,
         ContentReference reference,
         boolean isFeatureVerification,
         InstallMonitor monitor)
         throws CoreException {

         if (reference == null)
             return result;

         // if parent knows how to verify, ask the parent first
 if (getParent() != null) {
             IVerificationResult vr =
                 getParent().verify(feature, reference, isFeatureVerification, monitor);
             if (vr.getVerificationCode() != IVerificationResult.TYPE_ENTRY_UNRECOGNIZED)
                 return vr;
         }

         // the parent couldn't verify
 setMonitor(monitor);
         init(feature, reference);
         result.isFeatureVerification(isFeatureVerification);

         if (jarFile!=null) {
                 result = verify(jarFile.getAbsolutePath(), reference.getIdentifier());
         } else {
             result.setVerificationCode(IVerificationResult.TYPE_ENTRY_UNRECOGNIZED);
         }

         return result;
     }

     /*
      *
      */
     private JarVerificationResult verify(String file, String identifier) {

         try {

             // verify integrity
 verifyIntegrity(file, identifier);

             // do not close input stream
 // as verifyIntegrity already did it

             //if user already said yes
 result.alreadySeen(alreadyValidated());

             // verify source certificate
 if (result.getVerificationCode()
                 == IVerificationResult.TYPE_ENTRY_SIGNED_UNRECOGNIZED) {
                 verifyAuthentication();
             }

             // save the fact the file is not signed, so the user will not be prompted again
 if (result.getVerificationCode()
                 == IVerificationResult.TYPE_ENTRY_NOT_SIGNED) {
                 acceptUnsignedFiles = true;
             }

         } catch (Exception e) {
             result.setVerificationCode(IVerificationResult.UNKNOWN_ERROR);
             result.setResultException(e);
         }

         if (monitor != null) {
             monitor.worked(1);
             if (monitor.isCanceled()) {
                 result.setVerificationCode(IVerificationResult.VERIFICATION_CANCELLED);
             }
         }

         return result;
     }

     /*
      * Verifies that each file has at least one certificate
      * valid in the keystore
      *
      * At least one certificate from each Certificate Array
      * of the Jar file must be found in the known Certificates
      */
     private void verifyAuthentication() throws CoreException {

         CertificatePair[] entries = result.getRootCertificates();
         boolean certificateFound = false;

         // If all the certificate of an entry are
 // not found in the list of known certifcate
 // the certificate is not trusted by any keystore.
 for (int i = 0; i < entries.length; i++) {
             certificateFound = existsInKeystore(entries[i].getRoot());
             if (certificateFound) {
                 result.setVerificationCode(IVerificationResult.TYPE_ENTRY_SIGNED_RECOGNIZED);
                 result.setFoundCertificate(entries[i]);
                 return;
             }
         }
     }

     /*
      * Verifies the integrity of the JAR
      */
     private void verifyIntegrity(String file, String identifier) {

         JarFile jarFile = null;

         try {
             // If the JAR is signed and not valid
 // a security exception will be thrown
 // while reading it
 jarFile = new JarFile (file, true);
             List filesInJar = readJarFile(jarFile, identifier);

             // you have to read all the files once
 // before getting the certificates
 if (jarFile.getManifest() != null) {
                 Iterator iter = filesInJar.iterator();
                 boolean certificateFound = false;
                 while (iter.hasNext()) {
                     JarEntry currentJarEntry = (JarEntry ) iter.next();
                     Certificate [] certs = currentJarEntry.getCertificates();
                     if ((certs != null) && (certs.length != 0)) {
                         certificateFound = true;
                         result.addCertificates(certs);
                     } else {
                         String jarEntryName = currentJarEntry.getName();
                         if (!jarEntryName.toUpperCase().startsWith(MANIFEST)
                             && !currentJarEntry.isDirectory()) {
                             // if the jarEntry is not in MANIFEST, consider the whole file unsigned
 break;
                         }

                     }
                 }

                 if (certificateFound)
                     result.setVerificationCode(IVerificationResult.TYPE_ENTRY_SIGNED_UNRECOGNIZED);
                 else
                     result.setVerificationCode(IVerificationResult.TYPE_ENTRY_NOT_SIGNED);
             } else {
                 Exception e = new Exception (NLS.bind(Messages.JarVerifier_InvalidFile, (new String [] { file })));
                 result.setResultException(e);
                 result.setVerificationCode(IVerificationResult.TYPE_ENTRY_NOT_SIGNED);
                 UpdateCore.warn(null,e);
             }
         } catch (SecurityException e) {
             // Jar file is signed
 // but content has changed since signed
 result.setVerificationCode(IVerificationResult.TYPE_ENTRY_CORRUPTED);
         } catch (InterruptedException e) {
             result.setVerificationCode(IVerificationResult.VERIFICATION_CANCELLED);
         } catch (Exception e) {
             result.setVerificationCode(IVerificationResult.UNKNOWN_ERROR);
             result.setResultException(e);
         } finally {
             if (jarFile!=null){
                 try {jarFile.close();} catch (IOException e){}
             }
         }

     }

     /*
      *
      */
     private boolean alreadyValidated() {

         if (result.getVerificationCode() == IVerificationResult.TYPE_ENTRY_NOT_SIGNED)
             return (acceptUnsignedFiles);

         if (getTrustedCertificates() != null) {
             Iterator iter = getTrustedCertificates().iterator();
             CertificatePair[] jarPairs = result.getRootCertificates();

             // check if this is not a user accepted certificate for this feature
 while (iter.hasNext()) {
                 CertificatePair trustedCertificate = (CertificatePair) iter.next();
                 for (int i = 0; i < jarPairs.length; i++) {
                     if (trustedCertificate.equals(jarPairs[i])) {
                         return true;
                     }
                 }
             }

             // if certificate pair not found in trusted add it for next time
 for (int i = 0; i < jarPairs.length; i++) {
                 addTrustedCertificate(jarPairs[i]);
             }
         }

         return false;
     }

     /*
      *
      */
     private void addTrustedCertificate(CertificatePair pair) {
         if (trustedCertificates == null)
             trustedCertificates = new ArrayList ();
         if (pair != null)
             trustedCertificates.add(pair);
     }

     /*
      *
      */
     private List getTrustedCertificates() {
         if (trustedCertificates == null)
             trustedCertificates = new ArrayList ();
         return trustedCertificates;
     }

     /**
      * @see IVerifier#setParent(IVerifier)
      */
     public void setParent(IVerifier parentVerifier) {
         super.setParent(parentVerifier);
         initialize();
     }

 }

